#if !defined(__CL_ENABLE_EXCEPTIONS)
#define __CL_ENABLE_EXCEPTIONS
#endif

#include "Benchmark.hpp"
#include "Profiler.hpp"
#include "cl.hpp"
#include <vector>
#include <iostream>
#include <QFile>
#include <QIODevice>

using namespace cl;

namespace {
    const char *BUILD_OPTIONS = "-Werror";// -cl-fast-relaxed-math";
    const char *CL_KHR_FP64 = "cl_khr_fp64";
    const char *CL_AMD_FP64 = "cl_amd_fp64";
    const char *BENCHMARK_FP32 = ":/res/kernel/benchmark_fp32.cl";
    const char *BENCHMARK_FP64 = ":/res/kernel/benchmark_fp64.cl";
    const std::size_t WORKITEMS = 1024*64;
    const std::size_t WORKITEMS_PER_WORKGROUP = 128; // Apple items limitation !?
}

const int Benchmark::BUILD_SUCCESS     = CL_BUILD_SUCCESS;
const int Benchmark::BUILD_NONE        = CL_BUILD_NONE;
const int Benchmark::BUILD_ERROR       = CL_BUILD_ERROR;
const int Benchmark::BUILD_IN_PROGRESS = CL_BUILD_IN_PROGRESS;
const char *Benchmark::KERNEL_FP32 = "fp32";
const char *Benchmark::KERNEL_FP64 = "fp64";

Benchmark::Benchmark(const Device &device, QObject *parent) :
    QObject(parent),
    context(0),
    program_fp32(0),
    program_fp64(0),
    hasDoubleOption(false),
    profilingTime(0.0),
    profilingPerformance(0.0) {
    try {
        context = new Context(std::vector<Device>(1, device), 0,
                              &Benchmark::contextNotify, this);
    } catch(Error &e) {
        std::cerr << "cl::Benchmark::Benchmark " <<
                     e.what() << "(" << e.err() << ")" << std::endl;
    }
}

Benchmark::~Benchmark(void) {
    if(context)      { delete context;      context        = 0; }
    if(program_fp32) { delete program_fp32; program_fp32   = 0; }
    if(program_fp64) { delete program_fp64; program_fp64   = 0; }
}

void CL_CALLBACK Benchmark::contextNotify(
        const char *errorInfo, const void *, std::size_t, void *object) {
    std::cerr << errorInfo << std::endl;
    Benchmark *thiz = dynamic_cast<Benchmark *>((Benchmark *)object);
    if(thiz) {
        static std::string message = std::string(errorInfo);
        emit thiz->contextError(message.c_str());
    }
}

void Benchmark::loadProgram(bool hasDouble) {
    if(!program_fp32 && !hasDouble) {
        program_fp32 = createProgramFormSource(BENCHMARK_FP32);
        buildProgram(program_fp32);
    }
    if(!program_fp64 && hasDouble) {
        hasDoubleOption = true;
        program_fp64 = createProgramFormSource(BENCHMARK_FP64);
        buildProgram(program_fp64);
    }
}

bool Benchmark::hasDouble(void) {
    if(context) {
        std::string extensions;
        std::size_t found;
        try {
            Device device = context->getInfo<CL_CONTEXT_DEVICES>().front();
            extensions = device.getInfo<CL_DEVICE_EXTENSIONS>();
        } catch(Error &e) {
            std::cerr << "cl::Benchmark::hasDouble " <<
                         e.what() << "(" << e.err() << ")" << std::endl;
        }
        found = extensions.find(CL_KHR_FP64);
        if(found != std::string::npos)
            return true;
        found = extensions.find(CL_AMD_FP64);
        if(found != std::string::npos)
            return true;
    }
    return false;
}

Program *Benchmark::createProgramFormSource(const char *sourceFile) {
    Program *result = 0;
    QFile _sourceFile(sourceFile);
    std::string sourceCode;
    if(_sourceFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
        QString source;
        sourceCode.clear();
        while(!_sourceFile.atEnd())
            source.append(_sourceFile.readLine());
        _sourceFile.close();
        sourceCode = source.toStdString();
    }
    if(!sourceCode.empty()) {
        Program::Sources source(
                    1, std::make_pair(sourceCode.c_str(), sourceCode.size()));
        try {
            result = new Program(*context, source);
        } catch(Error &e) {
            std::cerr << "cl::Benchmark::createProgramFromSource " <<
                         e.what() << "(" << e.err() << ")" << std::endl;
        }
    }
    return result;
}

void Benchmark::buildProgram(Program *program) {
    if(context && program) {
        try {
            std::vector<Device> devices = context->getInfo<CL_CONTEXT_DEVICES>();
            program->build(devices, BUILD_OPTIONS, &Benchmark::buildNotify, this);
        } catch(Error &e) {
            std::cerr << "cl::Benchmark::buildProgram " <<
                         e.what() << "(" << e.err() << ")" << std::endl;
            /*******************************************************************
             * INFO: Note that not all OpenCL compilers handle errors in the
             *       same way.
             *       Some may throw here an exception without calling the
             *       callback function, others may do not so and report the
             *       outcome in the callback function, others may do also both.
             * INFO: The build command is a synchronous operation on the NVIDIA
             *       platform!!
             ******************************************************************/
        }
    }
}

void CL_CALLBACK Benchmark::buildNotify(cl_program, void *object) {
    Benchmark *thiz = dynamic_cast<Benchmark *>((Benchmark *)object);
    if(thiz) {
        try {
            static cl_build_status status;
            static std::string     options;
            static std::string     log;
            Device device = thiz->context->getInfo<CL_CONTEXT_DEVICES>().front();
            Program *program = 0;
            if(!thiz->hasDoubleOption) {
                program = thiz->program_fp32;
            } else {
                program = thiz->program_fp64;
            }
            status  = program->getBuildInfo<CL_PROGRAM_BUILD_STATUS>(device);
            options = program->getBuildInfo<CL_PROGRAM_BUILD_OPTIONS>(device);
            log     = program->getBuildInfo<CL_PROGRAM_BUILD_LOG>(device);
            emit thiz->programBuilt(status, options.c_str(), log.c_str());
        } catch(Error &e) {
            std::cerr << "cl::Benchmark::buildNotify " <<
                         e.what() << "(" << e.err() << ")" << std::endl;
        }
    }
}

std::string Benchmark::getVectorKernelName(const std::string &kernelName,
                                           NVector dimension) {
    std::string result = std::string(kernelName);
    switch(dimension) {
    /*case N16: result.append("_16");
              break;*/
    case N8:  result.append("_8");
              break;
    case N4:  result.append("_4");
              break;
    case N2:  result.append("_2");
              break;
    default:  break;
    }
    return result;
}

void Benchmark::runTest(const char *name, NVector dimension) {
    std::string kernelName = std::string(name);
    std::string vectorKernelName = getVectorKernelName(kernelName, dimension);
    try {
        if(kernelName.compare(KERNEL_FP32) == 0) {
            Profiler<cl_float> profiler(
                        *context,
                        context->getInfo<CL_CONTEXT_DEVICES>().front(),
                        *program_fp32,
                        vectorKernelName.c_str(),
                        WORKITEMS);
            profiler.run(WORKITEMS_PER_WORKGROUP);
            profilingTime        = profiler.elapsedTime();
            profilingPerformance = profiler.performance();
        } else
        if((kernelName.compare(KERNEL_FP64) == 0) && hasDouble()) {
            Profiler<cl_double> profiler(
                        *context,
                        context->getInfo<CL_CONTEXT_DEVICES>().front(),
                        *program_fp64,
                        vectorKernelName.c_str(),
                        WORKITEMS);
            profiler.run(WORKITEMS_PER_WORKGROUP);
            profilingTime        = profiler.elapsedTime();
            profilingPerformance = profiler.performance();
        }
    } catch(Error &e) {
        std::cerr << "cl::Benchmark::runKernel " <<
                     e.what() << "(" << e.err() << ")" << std::endl;
    }
}

double Benchmark::getProfilingTime(void) {
    return profilingTime;
}

double Benchmark::getProfilingPerformance(void) {
    return profilingPerformance;
}
